eBay云计算“网”事|网络丢包篇
供稿 | eBay IE Cloud Team
作者 | 谢文利 & 李程远
编辑 | 顾欣怡本文5994字,预计阅读时间18分钟更多干货请关注“eBay技术荟”公众号导读
在eBay云计算“网”事 | 网络超时篇中,我们针对主机网络中的延时突发问题进行了分析,可以看到,基于eBPF的代码级别工具带来的定位方法改变以及效率提升,非常有助于定位和解决问题。而eBay IE Cloud Team的目标,正是针对这些常规问题运用相似的工具来快速定位到症结所在。本期“网”事,我们将关注网络的另外一个常见问题——丢包。
01
问题描述
eBay的某个大型应用在容器化到Kubernetes平台后,发现某个周期性数据下载业务会出现失败率相对之前较高的情况。如图1所示:
02
应用部属详情
该应用具有700多个Pod,形成如图2所示的3行240列的集群分布。第一行的Pod从外部下载大约40GB数据,存到本地磁盘卷上,该Pod同时会有其他进程读取数据,将数据发送给第二行相同列的Pod。第二行相同列的Pod通过socket接收数据,放到本地磁盘卷。有另外的进程,读取数据,将数据发送给第三行相同列的Pod。第三行的相同列的Pod会有socket接收数据,并存入本地卷。
数据的传输按块来进行,每个数据块大小是4M。当40G的数据在该列的所有机器上都传输完成时,则表示该列数据传输成功,否则失败。从多次测试下载任务的结果来看,出现问题的节点具有较强的随机性,并不会固定出错。图2 3行240列的Pod业务分布形式
(点击可查看大图)为了高效维护Pod状态并且进行Pod之间的通信,每个Pod都会运行在基于Akka(https://akka.io/)的分布式集群上。集群中的每一个成员,都被集群中的某些成员监控,当其中任何一个监测点检测到这个被监测成员不可达时,这个不可达信息就会通过gossip协议传播到其他成员,集群中的其它成员都会标记这个成员不可达。监控节点是否可达,则是通过心跳机制来进行。监测点每秒发送一次心跳报文(request),被监测成员就会回复该心跳报文(reply)。在节点之间,会进行系统消息的传递,如果系统消息不能被送达一个节点,则会导致系统的不一致,这个节点会被设置为隔离(quarnteed)状态,并且它再不能从不可达状态中回来。03
问题分析
1. 心跳报文
从应用的日志来看,出现该问题的Pod,都具有在Akka的分布式架构被设置为隔离状态的情况,当节点被隔离后,数据传输也会停止。我们发现节点被设置为隔离状态的节点,都出现了心跳报文不能正常通信的情况,如图3所示。
发生端发送心跳的request报文后,并没有收到对端回复的reply报文。于是我们首先检测节点的心跳报文是否有什么地方出现了异常。
图3 发送心跳的请求报文后,没有收到心跳回复
(点击可查看大图)
通过分析tcpdump数据,得知heartbeat报文数据具有固定的特征,并且在某个固定的端口上运行,所以我们在每个节点上部署eBPF程序,统计发往容器eth0端口的heartbeat心跳数据,并且每隔10秒就打印统计数据情况。该eBPF通过容器的ip地址,tcp端口号以及数据包长度等特征,来判断是否是心跳报文,并计数统计。经过统计发现,从某个Pod发出去的heartbeat报文,和到达对端Pod eth0端口的heartbeat包数量符合,而在应用的日志里面,并没有收到heartbeat报文的日志打印。从netstat -s显示的结果,也没有看到明显的丢包情况。Akka架构本身,具有数据包的分发功能,在收到数据后,再将其分发给具体的处理线程,所以很容易就怀疑是Akka框架本身出现问题,并没有将数据正确传递传递给处理线程。于是和应用方讨论是否在Akka架构上有什么行为异常,我们则继续从节点系统层面进行其他的分析。我们写了socket收发数据程序,模拟数据下载上传的行为,希望能够复现数据传输失败的现象。在几个节点上做测试,甚至将数据速率调整为业务发送数据量的两倍,也并没有相应的现象出现,复现失败。
在分析日志的时候,发现有些出错节点日志出现如图4显示时间不连续的情况,中间有几秒的时间并没有打出日志来。
图4 16:20:59.264 - 16:21:03.264 之间并没有日志输出(点击可查看大图)
因此在该时刻,应用行为可能出现异常,于是让应用人员排查该问题。而我们则查看在该时刻,系统是否出现了异常。从有些出错节点的metrics(图5)来看,在出现问题的时刻附近,会有系统每秒的pageout数量出现突发,超过1000k,也就是需要刷大于4G的数据到硬盘。
图5 pageOut/Sec存在burst
(点击可查看大图)
修改libcache_flush.so库,当read() 或者write()超过64M时,进行刷页,而不需要等到512M。提高刷页的频率,避免同时刷大量的脏页。 将vm.dirty_background_ratio从默认值10修改为1,该行为会导致更频繁刷新cache,避免脏页累积到vm.dirty_ratio的水平,而导致需要同步刷脏页,阻碍应用的执行。
应用方同事在应用上增加日志,记录数据块的发送和接收情况。结果发现在heartbeat报文出现问题之前,Pod之间传递业务数据的时候,已经出现问题了,具体现象是:
发送方发送数据块N的时候,调用write() 接口,但是一直没有发送出去。 接收方在接收数据块N-2,调用read() 接口,但是一直没有读取到数据。
图6 发送端队列淤积,数据发送不出去
(点击可查看大图)
输出时间不连续。本来设定为每秒都输出结果,但是出问题的时刻,时间从2019-12-04 12:57:12直接跳到了2019-12-04 12:57:20,程序的运行在此刻出了问题。该现象在某些节点上会出现,但并不是所有出问题的节点都有同样的现象。 skmem项带有的drop统计出现了突然增长,从841涨到了1533。这么短的时间,出现这么多包显然不正常,而且在很长的时间内都没有恢复。那么这些包都丢到了哪儿了呢?发送端有进行数据的重传,但是重传数据一直都被丢弃,导致发送端的窗口被占满而无法继续发送。
很幸运的是,此刻我们发现有方式可以重现该问题,将之前用于重现问题的小程序,部署到出现数据下载失败的机器上,有时候可以重现应用碰到的问题——发送端发送数据暂停,发送的socket队列里面有大量的数据存在,而接收端并没有接收到数据,接收的socket队列也为空。在数据接收端通过ss命令查看socket的丢包统计,也同样出现了瞬间的增长,而netstat -s显示的统计里面,同样没有出现很多的丢包情况。
通过模拟程序,我们能够在出现了问题的节点上重现之前的现象,这对于问题的定位提供了很大的帮助。于是在模拟问题的时候,同时在节点上运行bcc检查tcp丢包的tcpdrop工具,详见:
https://github.com/iovisor/bcc/blob/master/tools/tcpdrop.py
该工具基于eBPF,可以打印出TCP链接的丢包信息,并将丢包的时刻的函数调用栈进行打印。很疑惑的是,当通过ss命令显示socket链接出现大量丢包的时候,tcpdrop工具并没有打印出来相关的丢包信息。很显然,该工具并没有抓到所有的丢包信息。于是我们分析了下该bcc tcpdrop工具的实现:其通过kprobe 追踪了tcp_drop()的函数,在该函数调用的时候,记录下相关的链接信息,并且打印出调用栈。那为何该工具并没有抓取到所有的丢包信息呢?应该是有些地方丢包,并没有调用tcp_drop()函数,才会导致这样的情况。于是继续查看tcp的协议栈代码,发现在tcp_v4_rcv(),tcp_v4_do_rcv(), tcp_rcv_established()的tcp数据包处理函数中,都有直接调用__kfree_skb(), kfree_skb(),而不是调用tcp_drop()来对数据进行丢弃。在这些函数里面,也会有多种情况会导致调用kfree_skb()。结合netstat -s的结果,我们对丢包的地方进行排除:tcp_v4_do_rcv()中,针对已经处理established状态的链接,不会调用kfree_skb(), 所以不会在这个地方丢包。 tcp_rcv_established()中,在skb->len = tcp_header_len(不带数据的ack包)的条件下,才会调用__kfree_skb(),所以不会在这个地方丢包。
图9 多种情况会导致skb被丢弃和释放
(点击可查看大图)
排查了代码运行的实际情况,并且和相关的SNMP MIB信息相对照,列出几个重点的怀疑对象。基于eBPF的kprobe,对多个函数进行了probe,包括tcp_filter(), tcp_add_backlog()等,记录函数的调用和返回值。当skb属于指定的端口和ip时,会记录该函数的处理,最后统一输出该skb的函数处理情况。最终发现,之所以在socket的统计上看到大量丢包,是因为在tcp_v4_rcv()里,调用tcp_filter()函数返回为true,导致需要将skb丢弃。而在tcp_filter()里,会判断该skb->pfmemalloc是否为true,来决定是否将数据丢弃。如图10所示。图10 判断skb->pfmemalloc值来决定是否丢弃
(点击可查看大图)
此处会增加socket上的PFMEMALLOCDROP MIB的统计值,说明该数据被丢弃的时候,还是会在netstat 显示的SNMP MIB中体现出来,而不在其他没有记录SNMP MIB统计的地方丢弃。但是netstat -s显示出来的PFMEMALLOCDROP统计是加1,而ss -i显示的丢包,为何是数十倍的增量呢?在tcp_filter()判断需要将包进行丢弃后,会调用sk_drops_add()函数来增加socket的丢包统计,而sk_drops_add()里,并不是简单地对丢包进行加1,而是加该skb对应的gso_segs个数, 也就是该skb包含多少个mss分片的个数。图11 socket丢包统计
(点击可查看大图)
因为统计方式的不同,所以netstat -s显示的丢包统计和ss 显示的丢包信息不符合。在数据下载上传的过程中,数据包的长度会达到十几K的大小,因此ss显示的丢包个数,会是netstat -s显示出来统计值的数十倍。经过这个分析,数据丢包的原因也明确了。在出问题的时候,接收数据包的skb->pfmalloc=true, 导致了数据被tcp协议栈丢弃。那么我们需要搞懂pfmalloc是什么,为什么skb->pfmalloc 这个时候是true呢?对于这个问题,我们需要从Linux对内存的回收管理开始讲起。图12 内存zone的水线
(点击可查看大图)
如图12所示,内存的每个zone都具有high、low、min三条水线。zone的可用空闲内存页为其空余内存页减去给高端地址预留的部分。当可用的空闲内存页低于low水线但高于min水线时,说明内存的使用率比较高,需要进行内存回收。在内存页分配完成后,系统的kswapd进程将被唤醒,以执行内存回收操作。当可用空余内存回到high的水线后,内存回收就完成了。低地址端zone的可用空余内存页只有在高水线之上,才可以向高地址端的zone提供内存页。当可用的空闲内存低于min水线时,说明当前的内存使用量已经过载。如果应用程序申请分配内存,则会触发内存的直接回收操作,内存分配需要等待直接回收完成后才继续进行。很多系统进程,特别是像kswapd和内存回收直接相关的进程,在该情况下还要保证能够正常工作,因此需要带上PF_MALLOC的标记位,这样可以不受水线的限制而正常分配内存。默认情况下,系统总的min值通过/proc/sys/vm/min_free_kbytes来设置。该值和系统内总的内存值相关。根据系统总的min值,通过zone空间占有的比例来分配相应的min水线、low水线(默认是min水线的1.25倍)和high水线(默认是min水线的1.5 倍),用户可以通过/proc/zoneinfo来查看到详细的水线信息和可用页信息。从内存回收的行为来看,内存在配置的时候,要尽量避免空闲可用内存页到达直接回收的水线。在大规模申请内存的场景,内存的回收速度跟不上内存的分配速度,那么内存水线很可能会低于min水线。内核在给网络数据包分配skb的时候,发现当前的内存水线低于min水线,设置skb->pfmalloc=true。当tcp协议栈处理该类型的数据时,会根据socket类型,进行是否丢包并释放skb的操作。因此应用数据下载失败问题的发生,是因为系统在一段时间内一直处于内存紧缺状态,所以导致数据包一直被丢弃,最终应用停止发送数据,数据下载任务失败。那么为什么Linux在给网络数据分配skb的时候,需要加上pfmalloc的标志位而不是直接分配不出来然后将数据丢弃呢?感兴趣的读者可以参考: https://lwn.net/Articles/439298/
04
解决方案
在Linux上,我们通过 /proc/sys/vm/watermark_scale_factor 来配置不同内存水线之间的差值,让kswapd每次被触发回收内存时,可以一次回收更多的内存,尽量避免内存水线到达min水位。同时通过配置/proc/sys/vm/min_free_kbytes来抬高min水线,防止在极端情况下,系统没有内存可以使用,造成异常。
另外在节点上,可以通过部属eBPF程序来抓取系统的内存直接回收(direct_reclaim)事件,当有内存直接回收的事件发生时,说明内存的参数还需要继续优化。
05
总结—网络丢包篇
常见的网络丢包问题,一般都可以通过网卡丢包统计,/proc/net/softnet,netstat统计等方式来查看丢包情况,也比较容易查看出丢包的原因所在。但是这些统计信息并不能反应出具体链接的具体丢包原因,而通过eBPF,则可以从代码级别提供一个很好的解决方法。
在tcpdrop工具的基础上,我们开发了另外一个追踪tcp丢包原因的工具。该工具记录了数据包在内核中被处理过的部分函数,如果产生了丢包,则会打印相关函数信息和函数堆栈。这样根据处理的函数信息,再结合代码,就可以推算出tcp数据包的丢包原因,最差的情况下也可以定位到丢包的函数级别。从定位过程可以看出,对于相对常规的丢包统计,eBPF能让我们更深入剖析内核,让常规的粗糙统计结果更精细化,在有大量链接的场景下,该特性尤其重要。下篇我们就来看看在有几万个链接的Software LoadBalancer上,如何利用eBPF来定位重传问题。您可能还感兴趣:
重磅 | eBay提出强大的轻量级推荐算法——洛伦兹因子分解机
实战 | 利用Delta Lake使Spark SQL支持跨表CRUD操作
eBay大量优质职位,等的就是你